﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using BruteMerge.ExecutableLauncher.Attributes;

// https://github.com/dblock/resourcelib
using Mono.Cecil;

using Vestris.ResourceLib;

namespace BruteMerge.Merger
{
    internal class CustomCecilResolver : IAssemblyResolver
    {
        private readonly AssemblyDefinition[] _knownAssemblyes;

        public CustomCecilResolver(params AssemblyDefinition[] knownAssemblyes)
        {
            _knownAssemblyes = knownAssemblyes;
         
            Console.WriteLine("Known assemblies");
            foreach (var assemblyDefinition in knownAssemblyes)
            {
                Console.WriteLine(assemblyDefinition.Name);
            }
        }

        public AssemblyDefinition Resolve(AssemblyNameReference name)
        {
            Console.WriteLine("Cecil custom resolver: resolving {0} ...", name);
            return _knownAssemblyes.SingleOrDefault(a => a.Name.Name == name.Name);
        }

        public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
        {
            return Resolve(name);
        }

        public AssemblyDefinition Resolve(string fullName)
        {
            Console.WriteLine("Cecil custom resolver: resolving {0} ...", fullName);
            return _knownAssemblyes.SingleOrDefault(a => a.FullName == fullName);
        }

        public AssemblyDefinition Resolve(string fullName, ReaderParameters parameters)
        {
            return Resolve(fullName);
        }
    }

    public static class Program
    {
        private const string TEMPLATE_ASSMBLY = "template";
        private const string TEMPLATE_FILE = TEMPLATE_ASSMBLY + ".exe";

        private static string GetResourceName()
        {
            return Guid.NewGuid().ToString("n");
        }

        static int Main(string[] args)
        {
            var options = BruteMergeOptions.Parse(args);
            if (options == null) return 1;

            var templateFileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TEMPLATE_FILE);
            
            byte[] templateData = null;
            AssemblyProcessor assemblyProcessor = null;
            
            if (File.Exists(templateFileName))
            {
                templateData = File.ReadAllBytes(templateFileName);
                assemblyProcessor = new AssemblyProcessor(templateFileName, new CustomCecilResolver(AssemblyDefinition.ReadAssembly(templateFileName)));
            }
            else// then in embedded resources
            {
                // we getting it in a very hackish way
                // TODO: do a bootstract injection in startup file, not in launcher
                var stream = Assembly.GetCallingAssembly()
                    .GetManifestResourceStream(TEMPLATE_FILE);

                if (stream != null)
                {
                    templateData = new byte[stream.Length];
                    stream.Read(templateData, 0, (int)stream.Length);

                    // This is better than previsious solution
                    // but we must refactor it
                    // TODO: refactor!
                    assemblyProcessor = new AssemblyProcessor(templateData.CreateStream(),
                        new CustomCecilResolver(AssemblyDefinition.ReadAssembly(templateData.CreateStream())));
                }
            }
           
            if (templateData == null)
            {
                Console.Error.WriteLine("Cannot find template on disk or in embedded resources!");
                return 1;
            }

            var inputFilesFromDirectory = new List<string>();
            if (options.InputDirectory != null)
            {
                if (!Directory.Exists(options.InputDirectory))
                {
                    Console.Error.WriteLine("Input directory not exists: {0}", options.InputDirectory);
                    return 1;
                }

                Console.WriteLine("Input directory exists {0}", options.InputDirectory);
                inputFilesFromDirectory = Directory.GetFiles(options.InputDirectory, "*.*", SearchOption.AllDirectories).ToList();
            }

            var notFound = options.InputFiles.Concat(options.EntryFile).Where(f => !File.Exists(f)).ToArray();
            if (notFound.Any())
            {
                Console.Error.WriteLine("Some files not found: {0}", string.Join("; ", notFound));
                return 1;
            }

            var entryAssembly = new AssemblyDescription(options.EntryFile, isEntry: true);
            if (entryAssembly.Assembly.EntryPoint == null)
            {
                Console.Error.WriteLine("The entry assembly doesn't contains entry point");
                return 1;
            }

            var mergingAssemblies = options.InputFiles.Concat(inputFilesFromDirectory).Select(f => new AssemblyDescription(f));

            foreach (var assembly in mergingAssemblies.Concat(entryAssembly))
            {
                var resourceName = GetResourceName();

                Console.WriteLine("Assembly: {0}, {1}", assembly.AssemblyName, resourceName);

                assemblyProcessor.AddResource(resourceName, File.ReadAllBytes(assembly.FileName));
                assemblyProcessor.DefineAssemblyAttribute<ResourceAliasAttribute>(assembly.AssemblyName, resourceName);
                
                if (assembly.IsEntry)
                {
                    var entryClass = assembly.Assembly.EntryPoint.DeclaringType.FullName;
                    assemblyProcessor.DefineAssemblyAttribute<EntryPointAttribute>(assembly.AssemblyName, entryClass);
                }
            }

            if(options.Bootstrap) // injecting template file
            {
                assemblyProcessor.AddResource(TEMPLATE_FILE, templateData);
            }

            if (options.InheritVersion)
            {
                Console.Write("Inheriting version information attributes ... ");

                // Okay, this is mazafaka 
                // Todo: find more common/portable solution

                var asm = Assembly.LoadFrom(entryAssembly.FileName);

                asm.GetAttribute<AssemblyTitleAttribute>()
                    .Do(a => assemblyProcessor.DefineAssemblyAttribute<AssemblyTitleAttribute>(a.Title));

                asm.GetAttribute<AssemblyCompanyAttribute>()
                    .Do(a => assemblyProcessor.DefineAssemblyAttribute<AssemblyCompanyAttribute>(a.Company));

                asm.GetAttribute<AssemblyProductAttribute>()
                    .Do(a => assemblyProcessor.DefineAssemblyAttribute<AssemblyProductAttribute>(a.Product));

                asm.GetAttribute<AssemblyCopyrightAttribute>()
                    .Do(a => assemblyProcessor.DefineAssemblyAttribute<AssemblyCopyrightAttribute>(a.Copyright));

                asm.GetAttribute<AssemblyTrademarkAttribute>()
                    .Do(a => assemblyProcessor.DefineAssemblyAttribute<AssemblyTrademarkAttribute>(a.Trademark));

                asm.GetAttribute<AssemblyVersionAttribute>()
                    .Do(a => assemblyProcessor.DefineAssemblyAttribute<AssemblyVersionAttribute>(a.Version));
                
                asm.GetAttribute<AssemblyFileVersionAttribute>()
                    .Do(a => assemblyProcessor.DefineAssemblyAttribute<AssemblyFileVersionAttribute>(a.Version));
                
                asm.GetAttribute<AssemblyInformationalVersionAttribute>()
                                    .Do(a => assemblyProcessor.DefineAssemblyAttribute<AssemblyInformationalVersionAttribute>(a.InformationalVersion));


                Console.WriteLine("OK");
            }

            // defing flags (here was a IsConsole flag)
            var flags = ApplicationFlagsAttribute.Flags.None;
            if (options.DebugPrint) flags |= ApplicationFlagsAttribute.Flags.EnableDebugPrintInLoader; 
            assemblyProcessor.DefineAssemblyAttribute<ApplicationFlagsAttribute>(flags);
          
            // console
            if (options.ConsoleApplication) 
                assemblyProcessor.SetConsoleCubsystem();

            // To enable entry file == output file
            var tempOutputFile = Path.Combine(Path.GetDirectoryName(options.OutputFile), GetResourceName());

            Console.Write("Saving assembly ... ");
            assemblyProcessor.Save(tempOutputFile);
            Console.WriteLine("OK");

            
            // Coping unmanaged resources
            var inheritResourceTypes = new List<Kernel32.ResourceTypes>();
            if (options.InheritIcon)
            {
                inheritResourceTypes.Add(Kernel32.ResourceTypes.RT_GROUP_ICON);
                inheritResourceTypes.Add(Kernel32.ResourceTypes.RT_ICON);
            }
            if (options.InheritVersion)
            {
                inheritResourceTypes.Add(Kernel32.ResourceTypes.RT_VERSION);
            }
            if (options.InheritManifest)
            {
                inheritResourceTypes.Add(Kernel32.ResourceTypes.RT_MANIFEST);
            }

            if (inheritResourceTypes.Count > 0)
            {
                Console.Write("Inheriting Win32 resources: {0} ... ",
                              string.Join(", ", inheritResourceTypes.Select(t => t.ToString())));

                using (var entryFileWin32Resources = new ResourceInfo())
                {
                    entryFileWin32Resources.Load(options.EntryFile);

                    var resourceToSave = entryFileWin32Resources
                        .ResourceTypes
                        .Where(r => inheritResourceTypes.Contains(r.ResourceType))
                        .SelectMany(r => entryFileWin32Resources.Resources[r]);

                    foreach (var resource in resourceToSave)
                    {
                        resource.SaveTo(tempOutputFile);
                    }
                }

                Console.WriteLine("OK");
            }


            if (File.Exists(options.OutputFile)) 
                File.Delete(options.OutputFile);

            File.Move(tempOutputFile, options.OutputFile);

            Console.WriteLine("Done.");

            return 0;
        }
    }
}
